<?php

require_once "Zend/Socket.php";
require_once "Zend/Socket/Select.php";
require_once "Zend/Socket/Tcp/Connection/Raw/Interface.php";

/**
 * @category   Zend
 * @package    Zend_Socket
 * @copyright  Salomon BRYS
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 *
 * This class encapsulates the TCP Server workflow.
 */
class Zend_Socket_Tcp_Server implements Zend_Socket_Tcp_Connection_Raw_Interface
{
	/**
	 * The client class to use
	 *
	 * @var string
	 */
	private $_clientClassName = null;

	/**
	 * The socket used by the server to listen
	 *
	 * @var Zend_Socket
	 */
	private $_socket = null;

	/**
	 * @var Zend_Socket_Select
	 */
	private $_select = null;

	/**
	 * Set to false during the loop to stop it.
	 * @var bool
	 */
	private $_runLoop = true;

	/**
	 * Registered ticks.
	 * @var array[int]array[string]mixed
	 */
	private $_registeredTick = array();

	/**
	 * Constructor
	 * $clientClassName should be the name of a class implementing Zend_Socket_Tcp_Connection_Interface
	 *
	 * @param int $port
	 * @param string $clientClassName
	 */
	public function __construct($clientClassName, $port, $addr = '0.0.0.0', $backlog = 0)
	{
		if (!in_array('Zend_Socket_Tcp_Connection_Interface', class_implements($clientClassName))) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"The client class given to the server needs to implements Zend_Socket_Tcp_Connection_Interface or extends Zend_Socket_Tcp_Connection_Abstract."
			);
		}

		$this->_clientClassName = $clientClassName;

		$this->_socket = new Zend_Socket_Tcp(Zend_Socket::PROT_LISTEN);

		if (!$this->_socket->bind($addr, $port)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"Could not bind the server socket.",
				$this->_socket
			);
		}

		if (!$this->_socket->listen($backlog)) {
			require_once 'Zend/Socket/Exception.php';
			throw new Zend_Socket_Exception(
				"Could not listen the server socket.",
				$this->_socket
			);
		}

		$this->_select = new Zend_Socket_Select();
		$this->register($this);
	}

	/**
	 * Closes the socket if it has not been closed before.
	 */
	public function __destruct()
	{
		if ($this->Socket()->getState() != Zend_Socket::STATE_CLOSED) {
			$this->Socket()->close();
		}
	}

	/**
	 * Unused callback
	 *
	 * @see Zend/Socket/Tcp/Connection/Raw/Zend_Socket_Tcp_Connection_Raw_Interface::onRegister()
	 */
	public function onRegister(Zend_Socket_Tcp_Server $server)
	{
	}

	/**
	 * Closes the socket if it has not been closed before.
	 *
	 * @see Zend/Socket/Tcp/Connection/Raw/Zend_Socket_Tcp_Connection_Raw_Interface::onUnregister()
	 */
	public function onUnregister()
	{
		if (!$this->_socket->getState() != Zend_Socket::STATE_CLOSED) {
			$this->_socket->close();
		}
	}

	/**
	 * @see Zend/Socket/Tcp/Connection/Raw/Zend_Socket_Tcp_Connection_Raw_Interface::Tokenizer()
	 */
	public function Tokenizer()
	{
		return $this->_socket;
	}

	/**
	 * @see Zend/Socket/Holder/Zend_Socket_Holder_Interface::Socket()
	 */
	public function Socket()
	{
		return $this->_socket;
	}

	/**
	 * Register a client on the server
	 * @param Zend_Socket_Client_Interface $client
	 */
	public function register(Zend_Socket_Tcp_Connection_Raw_Interface $client)
	{
		if ($this->_select->register($client)) {
			$client->onRegister($this);
		}
	}

	/**
	 * Unregister a client on the server
	 * @param Zend_Socket_Client_Interface $client
	 */
	public function unregister(Zend_Socket_Tcp_Connection_Raw_Interface $client)
	{
		if ($this->_select->unregister($client)) {
			foreach ($this->_registeredTick as $id => $tick) {
				if (is_array($tick['callback']) && $tick['callback'][0] === $client) {
					unset($this->_registeredTick[$id]);
				}
			}
			$client->onUnregister();
		}
	}

	/**
	 * LOOOOOOOOOOOP !
	 */
	public function loop()
	{
		$this->_runLoop = true;
		while ($this->_runLoop && count($this->_select->getRegistered())) {
			$timeout = null;
			if (!empty($this->_registeredTick)) {
				$microtime = microtime(true);
				foreach ($this->_registeredTick as $tick) {
					$newtime = $tick['time'] - $microtime;
					if ($timeout === null || $timeout > $newtime) {
						$timeout = $newtime;
					}
				}
			}
			$clients = $this->_select->run($timeout);
			foreach ($clients['write'] as $client) {
				$client->Socket()->write();
			}
			foreach ($clients['read'] as $client) {
				$len = $client->Socket()->readOrAccept();
				if ($client->Tokenizer()->hasToken()) {
					$client->onToken();
				}
				if ($len == 0) {
					$this->unregister($client);
				}
			}
			if (!empty($this->_registeredTick)) {
				$microtime = microtime(true);
				foreach ($this->_registeredTick as $id => $tick) {
					if ($tick['time'] <= $microtime) {
						call_user_func_array($tick['callback'], $tick['args']);
						unset($this->_registeredTick[$id]);
					}
				}
			}
		}
	}

	/**
	 * Stops the loop
	 *
	 * @var bool $unregister Wether to unregister all the registered socket or not.
	 * @var bool $close Wether to close the server socket or not.
	 * @return void
	 */
	public function stop($unregister = true, $close = true)
	{
		$this->_runLoop = false;

		if ($unregister) {
			$registered = $this->_select->getRegistered();
			foreach ($registered as $client) {
				if ($client !== $this) {
					$this->unregister($client);
				}
			}
		}

		if ($close) {
			$this->unregister($this);
		}
	}

	/**
	 * @see Zend/Socket/Tcp/Connection/Raw/Zend_Socket_Tcp_Connection_Raw_Interface::onToken()
	 */
	public function onToken()
	{
		$clientClassName = $this->_clientClassName;
		while ($socket = $this->_socket->getAccept()) {
			$this->register(new $clientClassName($socket));
		}
	}

	/**
	 * Register the call of a callback at the given time.
	 * The microtime is a float (it is compared to the return of microtime(true)).
	 *
	 * @param float $microtime
	 * @param callback $callback
	 * @return int $id The id of the registered tick, to be used with unregisterTick().
	 */
	public function registerTick($microtime, $callback, $args = array())
	{
		return array_push($this->_registeredTick, array('time' => $microtime, 'callback' => $callback, 'args' => $args));
	}

	/**
	 * Unregister a tick with its ID.
	 *
	 * @param int $id
	 * @return bool
	 */
	public function unregisterTick($id)
	{
		if (isset($this->_registeredTick[$id])) {
			unset($this->_registeredTick[$id]);
			return true;
		}
		return false;
	}
}
